///////////////////////////////////////////////////////////////////////////////
//-----------------------------------------------------------------------------
#include <Shader/GLSLProgram.h>
#include <File/File.h>
#include <string.h>

//-----------------------------------------------------------------------------

GLSLProgram::GLSLProgram()
	:	GPUProgram()
	,	m_glslCombinedShader(0)
	,	m_glslVertexShader(0)
	,	m_glslFragmentShader(0)
{
}

//-----------------------------------------------------------------------------

GLSLProgram::GLSLProgram(File* sourceVS, File* sourceFS, const char *vertexFunc, const char *fragFunc)
{
	GLSLProgram();
	(void)this->Load(sourceVS, sourceFS, vertexFunc, fragFunc);
}

//-----------------------------------------------------------------------------

bool GLSLProgram::Load(File* sourceVS, File* sourceFS, const char *vertexFunc, const char *fragFunc, const char *argsV, const char *argsF)
{
	// Am incarcat deja un program
	if (m_glslVertexShader || m_glslFragmentShader)
	{
		return false;
	}

	// Nu am primit nici un fisier valid
	if ((!sourceVS || !sourceVS->IsOpen()) && (!sourceFS || !sourceFS->IsOpen()))
	{
		return false;
	}

	if( !vertexFunc || !fragFunc )
	{
		return false;
	}

	// Cream contextul GLSL
	m_glslCombinedShader = glCreateProgram();
	GL_CHECK_ERRORS();
	if (m_glslCombinedShader == 0)
	{
		return false; // A esuat crearea contextului
	}
	
	// Citim continutul programul vertex din fisier si cream vertex shader-ul
	if (sourceVS && sourceVS->IsOpen())
	{
		m_glslVertexShader = glCreateShader(GL_VERTEX_SHADER);
		GL_CHECK_ERRORS();
		if( m_glslVertexShader == 0)
		{
			return false;
		}

		sourceVS->Seek(0);
		u32 size = sourceVS->GetSize();
		u32 offset = strlen( argsV );
		if( offset )
		{
			offset += 1;
			size += offset;
		}
		GLchar* sourceBuffer = MGL_NEW GLchar[size + 1];
		ASSERT( sourceBuffer );
		if( offset )
		{
			memcpy( sourceBuffer, argsV, offset-1 );
			sourceBuffer[offset-1] = '\n';
		}
		sourceVS->Read(sourceBuffer+offset, 1, size-offset);
		sourceBuffer[size] = 0;	// Adaugam terminatorul de sir

		glShaderSource(m_glslVertexShader, 1, (const GLchar**)&sourceBuffer, NULL);
		GL_CHECK_ERRORS();

		// Eliberam resursele
		SAFE_DEL_ARRAY( sourceBuffer );
	}

	// Citim continutul programul vertex din fisier si cream fragment shader-ul
	if (sourceFS && sourceFS->IsOpen())
	{
		// Cream intai un fragment profile
		m_glslFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
		GL_CHECK_ERRORS();
		if (m_glslFragmentShader == 0)
		{
			// Nu exista un profil valid
			return false;
		}

		sourceFS->Seek(0);
		u32 size = sourceFS->GetSize();
		u32 offset = strlen( argsF );
		if( offset )
		{
			offset += 1;
			size += offset;
		}
		GLchar* sourceBuffer = MGL_NEW GLchar[size + 1];
		ASSERT( sourceBuffer );
		if( offset )
		{
			memcpy( sourceBuffer, argsF, offset-1 );
			sourceBuffer[offset-1] = '\n';
		}
		sourceFS->Read(sourceBuffer+offset, 1, size-offset);
		sourceBuffer[size] = 0;	// Adaugam terminatorul de sir

		// Cream programul GLSL
		glShaderSource(m_glslFragmentShader, 1, (const GLchar**)&sourceBuffer, NULL);
		GL_CHECK_ERRORS();

		// Eliberam resursele
		SAFE_DEL_ARRAY( sourceBuffer );
	}

	return true; // succes
}

//-----------------------------------------------------------------------------

bool GLSLProgram::Compile()
{
	int tmp = 0;
	glGetShaderiv(m_glslVertexShader, GL_COMPILE_STATUS, &tmp);
	GL_CHECK_ERRORS();
	// Compilam intai vertex shader-ul daca exista
	if (m_glslVertexShader && tmp == GL_FALSE)
	{
		// COmpilam shader-ul
		glCompileShader(m_glslVertexShader);
		GL_CHECK_ERRORS();
		
		// Verificam compilarea
		glGetShaderiv(m_glslVertexShader, GL_COMPILE_STATUS, &tmp);
		GL_CHECK_ERRORS();
		if (tmp == GL_FALSE)
		{
			// Logam eroarea si iesim
			glGetShaderiv(m_glslVertexShader, GL_INFO_LOG_LENGTH, &tmp);
			GL_CHECK_ERRORS();
			char *strError = MGL_NEW char[tmp];
			if( strError )
			{
				glGetShaderInfoLog(m_glslVertexShader, tmp, &tmp, strError);
				GL_CHECK_ERRORS();
				DEBUG_ERR("%s\n", strError );
				SAFE_DEL_ARRAY( strError );
			}

			return false;
		}
		else
		{
			DEBUG_OUT("GLSL vertex shader compilation successful\n");
		}
	}
	
	// Compilam fragment shader-ul
	glGetShaderiv(m_glslFragmentShader, GL_COMPILE_STATUS, &tmp);
	GL_CHECK_ERRORS();
	if (m_glslFragmentShader && tmp == GL_FALSE)
	{
		// Compilam shader-ul
		glCompileShader(m_glslFragmentShader);
		GL_CHECK_ERRORS();
		
		// Verificam compilarea
		glGetShaderiv(m_glslFragmentShader, GL_COMPILE_STATUS, &tmp);
		GL_CHECK_ERRORS();
		if (tmp == GL_FALSE)
		{
			// Logam eroarea si iesim
			glGetShaderiv(m_glslFragmentShader, GL_INFO_LOG_LENGTH, &tmp);
			GL_CHECK_ERRORS();
			char *strError = MGL_NEW char[tmp];
			if( strError )
			{
				glGetShaderInfoLog(m_glslFragmentShader, tmp, &tmp, strError);
				GL_CHECK_ERRORS();
				DEBUG_ERR("%s\n", strError );
				SAFE_DEL_ARRAY( strError );
			}

			return false;
		}
		else
		{
			DEBUG_OUT("GLSL fragment shader compilation successful\n");
		}
	}

	return true; // Succes
}

//-----------------------------------------------------------------------------

bool GLSLProgram::Link()
{
	// Combinam vertex si fragment shader-ul intr-un singur shader(program);
	// numai daca exista si vertex si fragment shader
	if (m_glslVertexShader && m_glslFragmentShader)
	{
		glAttachShader(m_glslCombinedShader, m_glslVertexShader);
		GL_CHECK_ERRORS();
		glAttachShader(m_glslCombinedShader, m_glslFragmentShader);
		GL_CHECK_ERRORS();
	}

	// Incarcam shader-ul, pregatind-ul pt. binding
	glLinkProgram(m_glslCombinedShader);
	GL_CHECK_ERRORS();

	GLint tmp = 0;
	// Verificam compilarea
	glGetProgramiv(m_glslCombinedShader, GL_LINK_STATUS, &tmp);
	GL_CHECK_ERRORS();
	if (tmp == GL_FALSE)
	{
		// Logam eroarea si iesim
		glGetProgramiv(m_glslCombinedShader, GL_INFO_LOG_LENGTH, &tmp);
		GL_CHECK_ERRORS();
		char *strError = MGL_NEW char[tmp];
		if( strError )
		{
			glGetProgramInfoLog(m_glslCombinedShader, tmp, &tmp, strError);
			GL_CHECK_ERRORS();
			DEBUG_ERR("%s\n", strError );
			SAFE_DEL_ARRAY( strError );
		}

		return false;
	}

	DEBUG_OUT("GLSL shaders linking successful\n");
	return true;
}

//-----------------------------------------------------------------------------

void GLSLProgram::Use(bool use)
{
	if (!m_glslCombinedShader)
	{
		return;
	}
	
	if (use && !m_inUse)
	{
		m_inUse = true;

		// Activarea programului propriu-zisa
		glUseProgram(m_glslCombinedShader);
		GL_CHECK_ERRORS();

		processSavedVars();
	}
	else if(!use && m_inUse)
	{
		m_inUse = false;

		// Dezactivam programul
		glUseProgram(0);
		GL_CHECK_ERRORS();
	}
}

//-----------------------------------------------------------------------------

bool GLSLProgram::Validate()
{
	glValidateProgram(m_glslCombinedShader);
	GL_CHECK_ERRORS();
	int tmp = 0;
	glGetProgramiv(m_glslCombinedShader, GL_VALIDATE_STATUS, &tmp);
	GL_CHECK_ERRORS();
	if( tmp == GL_FALSE )
	{
		glGetProgramiv(m_glslCombinedShader, GL_INFO_LOG_LENGTH, &tmp);
		GL_CHECK_ERRORS();
		char *log = MGL_NEW char[tmp];
		if( log )
		{
			glGetProgramInfoLog(m_glslCombinedShader, tmp, &tmp, log);
			GL_CHECK_ERRORS();
			DEBUG_ERR("GLSL program validate info: %s\n", log);
			SAFE_DEL_ARRAY( log );
		}
	}
	return tmp == GL_TRUE;
}

//-----------------------------------------------------------------------------

void GLSLProgram::LoadParameters()
{
	if (m_glslCombinedShader == 0)
	{
		return; // Nu avem niciun program incarcat
	}

	GLint m_noAttrib,m_noUniform;
	GLint length1,length2,size;
	GLenum type;
	std::string name;
	Param temp;
	glGetProgramiv(m_glslCombinedShader,GL_ACTIVE_ATTRIBUTES,&m_noAttrib);
	GL_CHECK_ERRORS();
	glGetProgramiv(m_glslCombinedShader,GL_ACTIVE_UNIFORMS,&m_noUniform);
	GL_CHECK_ERRORS();
	glGetProgramiv(m_glslCombinedShader,GL_ACTIVE_ATTRIBUTE_MAX_LENGTH,&length1);
	GL_CHECK_ERRORS();
	glGetProgramiv(m_glslCombinedShader,GL_ACTIVE_UNIFORM_MAX_LENGTH,&length2);
	GL_CHECK_ERRORS();

	if( length1 < length2 ) length1 = length2;
	GLchar* buff = MGL_NEW GLchar[length1];

	for(GLint i = 0; i < m_noAttrib; i++)
	{
		glGetActiveAttrib(m_glslCombinedShader,i,length1,&length2,&size,&type,buff);
		GL_CHECK_ERRORS();
		name = std::string(buff);
		temp.isAttirb = true;
		temp.location = glGetAttribLocation(m_glslCombinedShader,buff);
		temp.size = size;
		temp.type = type;
		m_glslParametersMap[name] = temp;
	}

	for(GLint i = 0; i < m_noUniform; i++)
	{
		glGetActiveUniform(m_glslCombinedShader,i,length1,&length2,&size,&type,buff);
		GL_CHECK_ERRORS();
		name = std::string(buff);
		temp.isAttirb = false;
		temp.location = glGetUniformLocation(m_glslCombinedShader,buff);
		temp.size = size;
		temp.type = type;
		m_glslParametersMap[name] = temp;
	}

	SAFE_DEL_ARRAY( buff );
}

//-----------------------------------------------------------------------------

bool GLSLProgram::HasParameter(const char* paramName)
{
	std::string parameterName(paramName);
	if (m_glslParametersMap.find(parameterName) != m_glslParametersMap.end())
	{
		return true;
	}
	return false;
}

//-----------------------------------------------------------------------------

GLSLProgram::Param GLSLProgram::GetParameter(const char* paramName)
{
	// Folosim pointerii la parametri deja stocati
	std::string parameterName(paramName);
	if (m_glslParametersMap.find(parameterName) != m_glslParametersMap.end())
	{
		return m_glslParametersMap[parameterName];
	}

	Param temp;
	temp.location = glGetUniformLocation(m_glslCombinedShader,paramName);
	temp.isAttirb = false;
	if( !temp )
	{
		temp.location = glGetAttribLocation(m_glslCombinedShader,paramName);
		temp.isAttirb = true;
	}

	if( !temp )
	{
		DEBUG_ERR( "[GLSLProgram] Could not find parameter '%s' for shader '%s'!\n", paramName, GetFilename().c_str() );
	}

	temp.size = 0;
	temp.type = 0;
	std::string name = std::string(paramName);
	m_glslParametersMap[name] = temp;

	return temp;
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter1i(const char* paramName, int value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				glVertexAttrib1s(param.location, value);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform1i(param.location,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(0,param.isAttirb,param.location,value));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter2i(const char* paramName, int* value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				GLshort v[2];
				v[0] = value[0];
				v[1] = value[1];
				glVertexAttrib2sv(param.location, v);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform2iv(param.location,1,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(1,param.isAttirb,param.location,value,2));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter3i(const char* paramName, int* value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				GLshort v[2];
				v[0] = value[0];
				v[1] = value[1];
				v[2] = value[2];
				glVertexAttrib3sv(param.location, v);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform3iv(param.location,1,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(2,param.isAttirb,param.location,value,3));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter4i(const char* paramName, int* value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				GLshort v[4];
				v[0] = value[0];
				v[1] = value[1];
				v[2] = value[2];
				v[3] = value[3];
				glVertexAttrib4sv(param.location, v);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform4iv(param.location,1,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(3,param.isAttirb,param.location,value,4));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter1f(const char* paramName, float value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				glVertexAttrib1f(param.location, value);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform1f(param.location,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(4,param.isAttirb,param.location,value));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter2f(const char* paramName, float* value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				glVertexAttrib2fv(param.location, value);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform2fv(param.location,1,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(5,param.isAttirb,param.location,value,2));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter3f(const char* paramName, float* value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				glVertexAttrib3fv(param.location, value);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform3fv(param.location,1,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(6,param.isAttirb,param.location,value,3));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameter4f(const char* paramName, float* value)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		if( m_inUse )
		{
			if(param.isAttirb)
			{
				glVertexAttrib4fv(param.location, value);
				GL_CHECK_ERRORS();
			}
			else
			{
				glUniform4fv(param.location,1,value);
				GL_CHECK_ERRORS();
			}
		}
		else
		{
			m_glslShaderVarSaves.push_back(_shader_var_glsl_(7,param.isAttirb,param.location,value,4));
		}
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameterTexture(const char* paramName, GLuint textureHandle, GLuint textureUnit)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		glUniform1i(param.location, textureUnit);
		GL_CHECK_ERRORS();
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::EnableParameterTexture(const char* paramName)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		//cgGLEnableTextureParameter(param);
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::SetParameterMatrixf(const char* paramName, GLfloat *mat)
{
	Param param = this->GetParameter(paramName);
	if (param)
	{
		glUniformMatrix4fv( param.location, 1, GL_FALSE, mat );
	}
}

//-----------------------------------------------------------------------------

void GLSLProgram::Delete()
{
	this->Use(false);
	if (m_glslVertexShader)
	{
		if(m_glslCombinedShader)
		{
			glDetachShader(m_glslCombinedShader,m_glslVertexShader);
		}
		glDeleteShader(m_glslVertexShader);
		GL_CHECK_ERRORS();
	}
	if (m_glslFragmentShader)
	{
		if(m_glslCombinedShader)
		{
			glDetachShader(m_glslCombinedShader,m_glslFragmentShader);
		}
		glDeleteShader(m_glslFragmentShader);
		GL_CHECK_ERRORS();
	}
	if (m_glslCombinedShader)
	{
		glDeleteProgram(m_glslCombinedShader);
		GL_CHECK_ERRORS();
	}
}

//-----------------------------------------------------------------------------

GLSLProgram::~GLSLProgram(void)
{
	this->Delete();	
}

//-----------------------------------------------------------------------------

void	GLSLProgram::processSavedVars()
{
	while(m_glslShaderVarSaves.size())
	{
		_shader_var_glsl_ sh = m_glslShaderVarSaves.back();
		m_glslShaderVarSaves.pop_back();

		switch(sh.func)
		{
		case 0:
			{
				if( sh.attrib )
				{
					GLshort v = sh.value.vInt[0];
					glVertexAttrib1s( sh.loc, v );
				}
				else
				{
					glUniform1iv( sh.loc, 1, sh.value.vInt );
				}
			}
			break;

		case 1:
			{
				if( sh.attrib )
				{
					GLshort v[2];
					v[0] = sh.value.vInt[0];
					v[1] = sh.value.vInt[1];
					glVertexAttrib2sv( sh.loc, v );
				}
				else
				{
					glUniform2iv( sh.loc, 2, sh.value.vInt );
				}
			}
			break;

		case 2:
			{
				if( sh.attrib )
				{
					GLshort v[3];
					v[0] = sh.value.vInt[0];
					v[1] = sh.value.vInt[1];
					v[2] = sh.value.vInt[2];
					glVertexAttrib3sv( sh.loc, v );
				}
				else
				{
					glUniform3iv( sh.loc, 3, sh.value.vInt );
				}
			}
			break;

		case 3:
			{
				if( sh.attrib )
				{
					GLshort v[4];
					v[0] = sh.value.vInt[0];
					v[1] = sh.value.vInt[1];
					v[2] = sh.value.vInt[2];
					v[3] = sh.value.vInt[3];
					glVertexAttrib4sv( sh.loc, v );
				}
				else
				{
					glUniform4iv( sh.loc, 4, sh.value.vInt );
				}
			}
			break;

		case 4:
			{
				if( sh.attrib )
				{
					glVertexAttrib1fv( sh.loc, sh.value.vFloat );
				}
				else
				{
					glUniform1fv( sh.loc, 1, sh.value.vFloat );
				}
			}
			break;

		case 5:
			{
				if( sh.attrib )
				{
					glVertexAttrib2fv( sh.loc, sh.value.vFloat );
				}
				else
				{
					glUniform2fv( sh.loc, 2, sh.value.vFloat );
				}
			}
			break;

		case 6:
			{
				if( sh.attrib )
				{
					glVertexAttrib3fv( sh.loc, sh.value.vFloat );
				}
				else
				{
					glUniform3fv( sh.loc, 3, sh.value.vFloat );
				}
			}
			break;

		case 7:
			{
				if( sh.attrib )
				{
					glVertexAttrib4fv( sh.loc, sh.value.vFloat );
				}
				else
				{
					glUniform4fv( sh.loc, 4, sh.value.vFloat );
				}
			}
			break;

		default:
			ASSERT( false && "That function in not implemnted!" );
		}
	}
}

//-----------------------------------------------------------------------------
///////////////////////////////////////////////////////////////////////////////
